<?php
/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category   Zend
 * @package    Zend_Cache
 * @subpackage Zend_Cache_Backend
 * @copyright  Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 * @version    $Id: File.php 24030 2011-05-09 22:10:00Z mabe $
 */

/**
 * @see Zend_Cache_Backend_Interface
 */
require_once 'Zend/Cache/Backend/ExtendedInterface.php';

/**
 * @see Zend_Cache_Backend
 */
require_once 'Zend/Cache/Backend.php';

/**
 * @package    Zend_Cache
 * @subpackage Zend_Cache_Backend
 * @copyright  Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
class Zend_Cache_Backend_File extends Zend_Cache_Backend implements Zend_Cache_Backend_ExtendedInterface {
	/**
	 * Available options
	 *
	 * =====> (string) cache_dir :
	 * - Directory where to put the cache files
	 *
	 * =====> (boolean) file_locking :
	 * - Enable / disable file_locking
	 * - Can avoid cache corruption under bad circumstances but it doesn't work on multithread
	 * webservers and on NFS filesystems for example
	 *
	 * =====> (boolean) read_control :
	 * - Enable / disable read control
	 * - If enabled, a control key is embeded in cache file and this key is compared with the one
	 * calculated after the reading.
	 *
	 * =====> (string) read_control_type :
	 * - Type of read control (only if read control is enabled). Available values are :
	 * 'md5' for a md5 hash control (best but slowest)
	 * 'crc32' for a crc32 hash control (lightly less safe but faster, better choice)
	 * 'adler32' for an adler32 hash control (excellent choice too, faster than crc32)
	 * 'strlen' for a length only test (fastest)
	 *
	 * =====> (int) hashed_directory_level :
	 * - Hashed directory level
	 * - Set the hashed directory structure level. 0 means "no hashed directory
	 * structure", 1 means "one level of directory", 2 means "two levels"...
	 * This option can speed up the cache only when you have many thousands of
	 * cache file. Only specific benchs can help you to choose the perfect value
	 * for you. Maybe, 1 or 2 is a good start.
	 *
	 * =====> (int) hashed_directory_umask :
	 * - Umask for hashed directory structure
	 *
	 * =====> (string) file_name_prefix :
	 * - prefix for cache files
	 * - be really carefull with this option because a too generic value in a system cache dir
	 * (like /tmp) can cause disasters when cleaning the cache
	 *
	 * =====> (int) cache_file_umask :
	 * - Umask for cache files
	 *
	 * =====> (int) metatadatas_array_max_size :
	 * - max size for the metadatas array (don't change this value unless you
	 * know what you are doing)
	 *
	 * @var array available options
	 */
	protected $_options = array ('cache_dir' => null, 'file_locking' => true, 'read_control' => true, 'read_control_type' => 'crc32', 'hashed_directory_level' => 0, 'hashed_directory_umask' => 0700, 'file_name_prefix' => 'zend_cache', 'cache_file_umask' => 0600, 'metadatas_array_max_size' => 100 );
	
	/**
	 * Array of metadatas (each item is an associative array)
	 *
	 * @var array
	 */
	protected $_metadatasArray = array ();
	
	/**
	 * Constructor
	 *
	 * @param  array $options associative array of options
	 * @throws Zend_Cache_Exception
	 * @return void
	 */
	public function __construct(array $options = array()) {
		parent::__construct ( $options );
		if ($this->_options ['cache_dir'] !== null) { // particular case for this option
			$this->setCacheDir ( $this->_options ['cache_dir'] );
		} else {
			$this->setCacheDir ( self::getTmpDir () . DIRECTORY_SEPARATOR, false );
		}
		if (isset ( $this->_options ['file_name_prefix'] )) { // particular case for this option
			if (! preg_match ( '~^[a-zA-Z0-9_]+$~D', $this->_options ['file_name_prefix'] )) {
				Zend_Cache::throwException ( 'Invalid file_name_prefix : must use only [a-zA-Z0-9_]' );
			}
		}
		if ($this->_options ['metadatas_array_max_size'] < 10) {
			Zend_Cache::throwException ( 'Invalid metadatas_array_max_size, must be > 10' );
		}
		if (isset ( $options ['hashed_directory_umask'] ) && is_string ( $options ['hashed_directory_umask'] )) {
			// See #ZF-4422
			$this->_options ['hashed_directory_umask'] = octdec ( $this->_options ['hashed_directory_umask'] );
		}
		if (isset ( $options ['cache_file_umask'] ) && is_string ( $options ['cache_file_umask'] )) {
			// See #ZF-4422
			$this->_options ['cache_file_umask'] = octdec ( $this->_options ['cache_file_umask'] );
		}
	}
	
	/**
	 * Set the cache_dir (particular case of setOption() method)
	 *
	 * @param  string  $value
	 * @param  boolean $trailingSeparator If true, add a trailing separator is necessary
	 * @throws Zend_Cache_Exception
	 * @return void
	 */
	public function setCacheDir($value, $trailingSeparator = true) {
		if (! is_dir ( $value )) {
			Zend_Cache::throwException ( 'cache_dir must be a directory' );
		}
		if (! is_writable ( $value )) {
			Zend_Cache::throwException ( 'cache_dir is not writable' );
		}
		if ($trailingSeparator) {
			// add a trailing DIRECTORY_SEPARATOR if necessary
			$value = rtrim ( realpath ( $value ), '\\/' ) . DIRECTORY_SEPARATOR;
		}
		$this->_options ['cache_dir'] = $value;
	}
	
	/**
	 * Test if a cache is available for the given id and (if yes) return it (false else)
	 *
	 * @param string $id cache id
	 * @param boolean $doNotTestCacheValidity if set to true, the cache validity won't be tested
	 * @return string|false cached datas
	 */
	public function load($id, $doNotTestCacheValidity = false) {
		if (! ($this->_test ( $id, $doNotTestCacheValidity ))) {
			// The cache is not hit !
			return false;
		}
		$metadatas = $this->_getMetadatas ( $id );
		$file = $this->_file ( $id );
		$data = $this->_fileGetContents ( $file );
		if ($this->_options ['read_control']) {
			$hashData = $this->_hash ( $data, $this->_options ['read_control_type'] );
			$hashControl = $metadatas ['hash'];
			if ($hashData != $hashControl) {
				// Problem detected by the read control !
				$this->_log ( 'Zend_Cache_Backend_File::load() / read_control : stored hash and computed hash do not match' );
				$this->remove ( $id );
				return false;
			}
		}
		return $data;
	}
	
	/**
	 * Test if a cache is available or not (for the given id)
	 *
	 * @param string $id cache id
	 * @return mixed false (a cache is not available) or "last modified" timestamp (int) of the available cache record
	 */
	public function test($id) {
		clearstatcache ();
		return $this->_test ( $id, false );
	}
	
	/**
	 * Save some string datas into a cache record
	 *
	 * Note : $data is always "string" (serialization is done by the
	 * core not by the backend)
	 *
	 * @param  string $data             Datas to cache
	 * @param  string $id               Cache id
	 * @param  array  $tags             Array of strings, the cache record will be tagged by each string entry
	 * @param  int    $specificLifetime If != false, set a specific lifetime for this cache record (null => infinite lifetime)
	 * @return boolean true if no problem
	 */
	public function save($data, $id, $tags = array(), $specificLifetime = false) {
		clearstatcache ();
		$file = $this->_file ( $id );
		$path = $this->_path ( $id );
		if ($this->_options ['hashed_directory_level'] > 0) {
			if (! is_writable ( $path )) {
				// maybe, we just have to build the directory structure
				$this->_recursiveMkdirAndChmod ( $id );
			}
			if (! is_writable ( $path )) {
				return false;
			}
		}
		if ($this->_options ['read_control']) {
			$hash = $this->_hash ( $data, $this->_options ['read_control_type'] );
		} else {
			$hash = '';
		}
		$metadatas = array ('hash' => $hash, 'mtime' => time (), 'expire' => $this->_expireTime ( $this->getLifetime ( $specificLifetime ) ), 'tags' => $tags );
		$res = $this->_setMetadatas ( $id, $metadatas );
		if (! $res) {
			$this->_log ( 'Zend_Cache_Backend_File::save() / error on saving metadata' );
			return false;
		}
		$res = $this->_filePutContents ( $file, $data );
		return $res;
	}
	
	/**
	 * Remove a cache record
	 *
	 * @param  string $id cache id
	 * @return boolean true if no problem
	 */
	public function remove($id) {
		$file = $this->_file ( $id );
		$boolRemove = $this->_remove ( $file );
		$boolMetadata = $this->_delMetadatas ( $id );
		return $boolMetadata && $boolRemove;
	}
	
	/**
	 * Clean some cache records
	 *
	 * Available modes are :
	 *
	 * Zend_Cache::CLEANING_MODE_ALL (default)    => remove all cache entries ($tags is not used)
	 * Zend_Cache::CLEANING_MODE_OLD              => remove too old cache entries ($tags is not used)
	 * Zend_Cache::CLEANING_MODE_MATCHING_TAG     => remove cache entries matching all given tags
	 * ($tags can be an array of strings or a single string)
	 * Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG => remove cache entries not {matching one of the given tags}
	 * ($tags can be an array of strings or a single string)
	 * Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG => remove cache entries matching any given tags
	 * ($tags can be an array of strings or a single string)
	 *
	 * @param string $mode clean mode
	 * @param tags array $tags array of tags
	 * @return boolean true if no problem
	 */
	public function clean($mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array()) {
		// We use this protected method to hide the recursive stuff
		clearstatcache ();
		return $this->_clean ( $this->_options ['cache_dir'], $mode, $tags );
	}
	
	/**
	 * Return an array of stored cache ids
	 *
	 * @return array array of stored cache ids (string)
	 */
	public function getIds() {
		return $this->_get ( $this->_options ['cache_dir'], 'ids', array () );
	}
	
	/**
	 * Return an array of stored tags
	 *
	 * @return array array of stored tags (string)
	 */
	public function getTags() {
		return $this->_get ( $this->_options ['cache_dir'], 'tags', array () );
	}
	
	/**
	 * Return an array of stored cache ids which match given tags
	 *
	 * In case of multiple tags, a logical AND is made between tags
	 *
	 * @param array $tags array of tags
	 * @return array array of matching cache ids (string)
	 */
	public function getIdsMatchingTags($tags = array()) {
		return $this->_get ( $this->_options ['cache_dir'], 'matching', $tags );
	}
	
	/**
	 * Return an array of stored cache ids which don't match given tags
	 *
	 * In case of multiple tags, a logical OR is made between tags
	 *
	 * @param array $tags array of tags
	 * @return array array of not matching cache ids (string)
	 */
	public function getIdsNotMatchingTags($tags = array()) {
		return $this->_get ( $this->_options ['cache_dir'], 'notMatching', $tags );
	}
	
	/**
	 * Return an array of stored cache ids which match any given tags
	 *
	 * In case of multiple tags, a logical AND is made between tags
	 *
	 * @param array $tags array of tags
	 * @return array array of any matching cache ids (string)
	 */
	public function getIdsMatchingAnyTags($tags = array()) {
		return $this->_get ( $this->_options ['cache_dir'], 'matchingAny', $tags );
	}
	
	/**
	 * Return the filling percentage of the backend storage
	 *
	 * @throws Zend_Cache_Exception
	 * @return int integer between 0 and 100
	 */
	public function getFillingPercentage() {
		$free = disk_free_space ( $this->_options ['cache_dir'] );
		$total = disk_total_space ( $this->_options ['cache_dir'] );
		if ($total == 0) {
			Zend_Cache::throwException ( 'can\'t get disk_total_space' );
		} else {
			if ($free >= $total) {
				return 100;
			}
			return (( int ) (100. * ($total - $free) / $total));
		}
	}
	
	/**
	 * Return an array of metadatas for the given cache id
	 *
	 * The array must include these keys :
	 * - expire : the expire timestamp
	 * - tags : a string array of tags
	 * - mtime : timestamp of last modification time
	 *
	 * @param string $id cache id
	 * @return array array of metadatas (false if the cache id is not found)
	 */
	public function getMetadatas($id) {
		$metadatas = $this->_getMetadatas ( $id );
		if (! $metadatas) {
			return false;
		}
		if (time () > $metadatas ['expire']) {
			return false;
		}
		return array ('expire' => $metadatas ['expire'], 'tags' => $metadatas ['tags'], 'mtime' => $metadatas ['mtime'] );
	}
	
	/**
	 * Give (if possible) an extra lifetime to the given cache id
	 *
	 * @param string $id cache id
	 * @param int $extraLifetime
	 * @return boolean true if ok
	 */
	public function touch($id, $extraLifetime) {
		$metadatas = $this->_getMetadatas ( $id );
		if (! $metadatas) {
			return false;
		}
		if (time () > $metadatas ['expire']) {
			return false;
		}
		$newMetadatas = array ('hash' => $metadatas ['hash'], 'mtime' => time (), 'expire' => $metadatas ['expire'] + $extraLifetime, 'tags' => $metadatas ['tags'] );
		$res = $this->_setMetadatas ( $id, $newMetadatas );
		if (! $res) {
			return false;
		}
		return true;
	}
	
	/**
	 * Return an associative array of capabilities (booleans) of the backend
	 *
	 * The array must include these keys :
	 * - automatic_cleaning (is automating cleaning necessary)
	 * - tags (are tags supported)
	 * - expired_read (is it possible to read expired cache records
	 * (for doNotTestCacheValidity option for example))
	 * - priority does the backend deal with priority when saving
	 * - infinite_lifetime (is infinite lifetime can work with this backend)
	 * - get_list (is it possible to get the list of cache ids and the complete list of tags)
	 *
	 * @return array associative of with capabilities
	 */
	public function getCapabilities() {
		return array ('automatic_cleaning' => true, 'tags' => true, 'expired_read' => true, 'priority' => false, 'infinite_lifetime' => true, 'get_list' => true );
	}
	
	/**
	 * PUBLIC METHOD FOR UNIT TESTING ONLY !
	 *
	 * Force a cache record to expire
	 *
	 * @param string $id cache id
	 */
	public function ___expire($id) {
		$metadatas = $this->_getMetadatas ( $id );
		if ($metadatas) {
			$metadatas ['expire'] = 1;
			$this->_setMetadatas ( $id, $metadatas );
		}
	}
	
	/**
	 * Get a metadatas record
	 *
	 * @param  string $id  Cache id
	 * @return array|false Associative array of metadatas
	 */
	protected function _getMetadatas($id) {
		if (isset ( $this->_metadatasArray [$id] )) {
			return $this->_metadatasArray [$id];
		} else {
			$metadatas = $this->_loadMetadatas ( $id );
			if (! $metadatas) {
				return false;
			}
			$this->_setMetadatas ( $id, $metadatas, false );
			return $metadatas;
		}
	}
	
	/**
	 * Set a metadatas record
	 *
	 * @param  string $id        Cache id
	 * @param  array  $metadatas Associative array of metadatas
	 * @param  boolean $save     optional pass false to disable saving to file
	 * @return boolean True if no problem
	 */
	protected function _setMetadatas($id, $metadatas, $save = true) {
		if (count ( $this->_metadatasArray ) >= $this->_options ['metadatas_array_max_size']) {
			$n = ( int ) ($this->_options ['metadatas_array_max_size'] / 10);
			$this->_metadatasArray = array_slice ( $this->_metadatasArray, $n );
		}
		if ($save) {
			$result = $this->_saveMetadatas ( $id, $metadatas );
			if (! $result) {
				return false;
			}
		}
		$this->_metadatasArray [$id] = $metadatas;
		return true;
	}
	
	/**
	 * Drop a metadata record
	 *
	 * @param  string $id Cache id
	 * @return boolean True if no problem
	 */
	protected function _delMetadatas($id) {
		if (isset ( $this->_metadatasArray [$id] )) {
			unset ( $this->_metadatasArray [$id] );
		}
		$file = $this->_metadatasFile ( $id );
		return $this->_remove ( $file );
	}
	
	/**
	 * Clear the metadatas array
	 *
	 * @return void
	 */
	protected function _cleanMetadatas() {
		$this->_metadatasArray = array ();
	}
	
	/**
	 * Load metadatas from disk
	 *
	 * @param  string $id Cache id
	 * @return array|false Metadatas associative array
	 */
	protected function _loadMetadatas($id) {
		$file = $this->_metadatasFile ( $id );
		$result = $this->_fileGetContents ( $file );
		if (! $result) {
			return false;
		}
		$tmp = @unserialize ( $result );
		return $tmp;
	}
	
	/**
	 * Save metadatas to disk
	 *
	 * @param  string $id        Cache id
	 * @param  array  $metadatas Associative array
	 * @return boolean True if no problem
	 */
	protected function _saveMetadatas($id, $metadatas) {
		$file = $this->_metadatasFile ( $id );
		$result = $this->_filePutContents ( $file, serialize ( $metadatas ) );
		if (! $result) {
			return false;
		}
		return true;
	}
	
	/**
	 * Make and return a file name (with path) for metadatas
	 *
	 * @param  string $id Cache id
	 * @return string Metadatas file name (with path)
	 */
	protected function _metadatasFile($id) {
		$path = $this->_path ( $id );
		$fileName = $this->_idToFileName ( 'internal-metadatas---' . $id );
		return $path . $fileName;
	}
	
	/**
	 * Check if the given filename is a metadatas one
	 *
	 * @param  string $fileName File name
	 * @return boolean True if it's a metadatas one
	 */
	protected function _isMetadatasFile($fileName) {
		$id = $this->_fileNameToId ( $fileName );
		if (substr ( $id, 0, 21 ) == 'internal-metadatas---') {
			return true;
		} else {
			return false;
		}
	}
	
	/**
	 * Remove a file
	 *
	 * If we can't remove the file (because of locks or any problem), we will touch
	 * the file to invalidate it
	 *
	 * @param  string $file Complete file path
	 * @return boolean True if ok
	 */
	protected function _remove($file) {
		if (! is_file ( $file )) {
			return false;
		}
		if (! @unlink ( $file )) {
			# we can't remove the file (because of locks or any problem)
			$this->_log ( "Zend_Cache_Backend_File::_remove() : we can't remove $file" );
			return false;
		}
		return true;
	}
	
	/**
	 * Clean some cache records (protected method used for recursive stuff)
	 *
	 * Available modes are :
	 * Zend_Cache::CLEANING_MODE_ALL (default)    => remove all cache entries ($tags is not used)
	 * Zend_Cache::CLEANING_MODE_OLD              => remove too old cache entries ($tags is not used)
	 * Zend_Cache::CLEANING_MODE_MATCHING_TAG     => remove cache entries matching all given tags
	 * ($tags can be an array of strings or a single string)
	 * Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG => remove cache entries not {matching one of the given tags}
	 * ($tags can be an array of strings or a single string)
	 * Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG => remove cache entries matching any given tags
	 * ($tags can be an array of strings or a single string)
	 *
	 * @param  string $dir  Directory to clean
	 * @param  string $mode Clean mode
	 * @param  array  $tags Array of tags
	 * @throws Zend_Cache_Exception
	 * @return boolean True if no problem
	 */
	protected function _clean($dir, $mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array()) {
		if (! is_dir ( $dir )) {
			return false;
		}
		$result = true;
		$prefix = $this->_options ['file_name_prefix'];
		$glob = @glob ( $dir . $prefix . '--*' );
		if ($glob === false) {
			// On some systems it is impossible to distinguish between empty match and an error.
			return true;
		}
		foreach ( $glob as $file ) {
			if (is_file ( $file )) {
				$fileName = basename ( $file );
				if ($this->_isMetadatasFile ( $fileName )) {
					// in CLEANING_MODE_ALL, we drop anything, even remainings old metadatas files
					if ($mode != Zend_Cache::CLEANING_MODE_ALL) {
						continue;
					}
				}
				$id = $this->_fileNameToId ( $fileName );
				$metadatas = $this->_getMetadatas ( $id );
				if ($metadatas === FALSE) {
					$metadatas = array ('expire' => 1, 'tags' => array () );
				}
				switch ($mode) {
					case Zend_Cache::CLEANING_MODE_ALL :
						$res = $this->remove ( $id );
						if (! $res) {
							// in this case only, we accept a problem with the metadatas file drop
							$res = $this->_remove ( $file );
						}
						$result = $result && $res;
						break;
					case Zend_Cache::CLEANING_MODE_OLD :
						if (time () > $metadatas ['expire']) {
							$result = $this->remove ( $id ) && $result;
						}
						break;
					case Zend_Cache::CLEANING_MODE_MATCHING_TAG :
						$matching = true;
						foreach ( $tags as $tag ) {
							if (! in_array ( $tag, $metadatas ['tags'] )) {
								$matching = false;
								break;
							}
						}
						if ($matching) {
							$result = $this->remove ( $id ) && $result;
						}
						break;
					case Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG :
						$matching = false;
						foreach ( $tags as $tag ) {
							if (in_array ( $tag, $metadatas ['tags'] )) {
								$matching = true;
								break;
							}
						}
						if (! $matching) {
							$result = $this->remove ( $id ) && $result;
						}
						break;
					case Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG :
						$matching = false;
						foreach ( $tags as $tag ) {
							if (in_array ( $tag, $metadatas ['tags'] )) {
								$matching = true;
								break;
							}
						}
						if ($matching) {
							$result = $this->remove ( $id ) && $result;
						}
						break;
					default :
						Zend_Cache::throwException ( 'Invalid mode for clean() method' );
						break;
				}
			}
			if ((is_dir ( $file )) and ($this->_options ['hashed_directory_level'] > 0)) {
				// Recursive call
				$result = $this->_clean ( $file . DIRECTORY_SEPARATOR, $mode, $tags ) && $result;
				if ($mode == Zend_Cache::CLEANING_MODE_ALL) {
					// we try to drop the structure too
					@rmdir ( $file );
				}
			}
		}
		return $result;
	}
	
	protected function _get($dir, $mode, $tags = array()) {
		if (! is_dir ( $dir )) {
			return false;
		}
		$result = array ();
		$prefix = $this->_options ['file_name_prefix'];
		$glob = @glob ( $dir . $prefix . '--*' );
		if ($glob === false) {
			// On some systems it is impossible to distinguish between empty match and an error.
			return array ();
		}
		foreach ( $glob as $file ) {
			if (is_file ( $file )) {
				$fileName = basename ( $file );
				$id = $this->_fileNameToId ( $fileName );
				$metadatas = $this->_getMetadatas ( $id );
				if ($metadatas === FALSE) {
					continue;
				}
				if (time () > $metadatas ['expire']) {
					continue;
				}
				switch ($mode) {
					case 'ids' :
						$result [] = $id;
						break;
					case 'tags' :
						$result = array_unique ( array_merge ( $result, $metadatas ['tags'] ) );
						break;
					case 'matching' :
						$matching = true;
						foreach ( $tags as $tag ) {
							if (! in_array ( $tag, $metadatas ['tags'] )) {
								$matching = false;
								break;
							}
						}
						if ($matching) {
							$result [] = $id;
						}
						break;
					case 'notMatching' :
						$matching = false;
						foreach ( $tags as $tag ) {
							if (in_array ( $tag, $metadatas ['tags'] )) {
								$matching = true;
								break;
							}
						}
						if (! $matching) {
							$result [] = $id;
						}
						break;
					case 'matchingAny' :
						$matching = false;
						foreach ( $tags as $tag ) {
							if (in_array ( $tag, $metadatas ['tags'] )) {
								$matching = true;
								break;
							}
						}
						if ($matching) {
							$result [] = $id;
						}
						break;
					default :
						Zend_Cache::throwException ( 'Invalid mode for _get() method' );
						break;
				}
			}
			if ((is_dir ( $file )) and ($this->_options ['hashed_directory_level'] > 0)) {
				// Recursive call
				$recursiveRs = $this->_get ( $file . DIRECTORY_SEPARATOR, $mode, $tags );
				if ($recursiveRs === false) {
					$this->_log ( 'Zend_Cache_Backend_File::_get() / recursive call : can\'t list entries of "' . $file . '"' );
				} else {
					$result = array_unique ( array_merge ( $result, $recursiveRs ) );
				}
			}
		}
		return array_unique ( $result );
	}
	
	/**
	 * Compute & return the expire time
	 *
	 * @return int expire time (unix timestamp)
	 */
	protected function _expireTime($lifetime) {
		if ($lifetime === null) {
			return 9999999999;
		}
		return time () + $lifetime;
	}
	
	/**
	 * Make a control key with the string containing datas
	 *
	 * @param  string $data        Data
	 * @param  string $controlType Type of control 'md5', 'crc32' or 'strlen'
	 * @throws Zend_Cache_Exception
	 * @return string Control key
	 */
	protected function _hash($data, $controlType) {
		switch ($controlType) {
			case 'md5' :
				return md5 ( $data );
			case 'crc32' :
				return crc32 ( $data );
			case 'strlen' :
				return strlen ( $data );
			case 'adler32' :
				return hash ( 'adler32', $data );
			default :
				Zend_Cache::throwException ( "Incorrect hash function : $controlType" );
		}
	}
	
	/**
	 * Transform a cache id into a file name and return it
	 *
	 * @param  string $id Cache id
	 * @return string File name
	 */
	protected function _idToFileName($id) {
		$prefix = $this->_options ['file_name_prefix'];
		$result = $prefix . '---' . $id;
		return $result;
	}
	
	/**
	 * Make and return a file name (with path)
	 *
	 * @param  string $id Cache id
	 * @return string File name (with path)
	 */
	protected function _file($id) {
		$path = $this->_path ( $id );
		$fileName = $this->_idToFileName ( $id );
		return $path . $fileName;
	}
	
	/**
	 * Return the complete directory path of a filename (including hashedDirectoryStructure)
	 *
	 * @param  string $id Cache id
	 * @param  boolean $parts if true, returns array of directory parts instead of single string
	 * @return string Complete directory path
	 */
	protected function _path($id, $parts = false) {
		$partsArray = array ();
		$root = $this->_options ['cache_dir'];
		$prefix = $this->_options ['file_name_prefix'];
		if ($this->_options ['hashed_directory_level'] > 0) {
			$hash = hash ( 'adler32', $id );
			for($i = 0; $i < $this->_options ['hashed_directory_level']; $i ++) {
				$root = $root . $prefix . '--' . substr ( $hash, 0, $i + 1 ) . DIRECTORY_SEPARATOR;
				$partsArray [] = $root;
			}
		}
		if ($parts) {
			return $partsArray;
		} else {
			return $root;
		}
	}
	
	/**
	 * Make the directory strucuture for the given id
	 *
	 * @param string $id cache id
	 * @return boolean true
	 */
	protected function _recursiveMkdirAndChmod($id) {
		if ($this->_options ['hashed_directory_level'] <= 0) {
			return true;
		}
		$partsArray = $this->_path ( $id, true );
		foreach ( $partsArray as $part ) {
			if (! is_dir ( $part )) {
				@mkdir ( $part, $this->_options ['hashed_directory_umask'] );
				@chmod ( $part, $this->_options ['hashed_directory_umask'] ); // see #ZF-320 (this line is required in some configurations)
			}
		}
		return true;
	}
	
	/**
	 * Test if the given cache id is available (and still valid as a cache record)
	 *
	 * @param  string  $id                     Cache id
	 * @param  boolean $doNotTestCacheValidity If set to true, the cache validity won't be tested
	 * @return boolean|mixed false (a cache is not available) or "last modified" timestamp (int) of the available cache record
	 */
	protected function _test($id, $doNotTestCacheValidity) {
		$metadatas = $this->_getMetadatas ( $id );
		if (! $metadatas) {
			return false;
		}
		if ($doNotTestCacheValidity || (time () <= $metadatas ['expire'])) {
			return $metadatas ['mtime'];
		}
		return false;
	}
	
	/**
	 * Return the file content of the given file
	 *
	 * @param  string $file File complete path
	 * @return string File content (or false if problem)
	 */
	protected function _fileGetContents($file) {
		$result = false;
		if (! is_file ( $file )) {
			return false;
		}
		$f = @fopen ( $file, 'rb' );
		if ($f) {
			if ($this->_options ['file_locking'])
				@flock ( $f, LOCK_SH );
			$result = stream_get_contents ( $f );
			if ($this->_options ['file_locking'])
				@flock ( $f, LOCK_UN );
			@fclose ( $f );
		}
		return $result;
	}
	
	/**
	 * Put the given string into the given file
	 *
	 * @param  string $file   File complete path
	 * @param  string $string String to put in file
	 * @return boolean true if no problem
	 */
	protected function _filePutContents($file, $string) {
		$result = false;
		$f = @fopen ( $file, 'ab+' );
		if ($f) {
			if ($this->_options ['file_locking'])
				@flock ( $f, LOCK_EX );
			fseek ( $f, 0 );
			ftruncate ( $f, 0 );
			$tmp = @fwrite ( $f, $string );
			if (! ($tmp === FALSE)) {
				$result = true;
			}
			@fclose ( $f );
		}
		@chmod ( $file, $this->_options ['cache_file_umask'] );
		return $result;
	}
	
	/**
	 * Transform a file name into cache id and return it
	 *
	 * @param  string $fileName File name
	 * @return string Cache id
	 */
	protected function _fileNameToId($fileName) {
		$prefix = $this->_options ['file_name_prefix'];
		return preg_replace ( '~^' . $prefix . '---(.*)$~', '$1', $fileName );
	}

}
